//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#include <buola/buola.h>
#include <buola/io.h>

#include <buola/algorithm/split.h>
#include <buola/algorithm/container.h>
#include <buola/os/cprocess.h>
#include <buola/io/cpipe.h>
#include <buola/os/cenv.h>
#include <buola/io/filesystem.h>

#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <cstdlib>
#include <csignal>
#include <sys/stat.h>
#include <buola/os/csignalblocker.h>

namespace buola {

//TODO: add functinality to track the status of the process... whether it is not started, running, stopped or terminated
//the status would be updated upon receiving a signal or 
    
std::map<pid_t,CProcess*> CProcess::mAllProcesses;

////////////////////////////////////////////////////////////////
//////////////////// constructor & destructor //////////////////
////////////////////////////////////////////////////////////////

CProcess::CProcess()
    :   mOptions(OPTION_NONE)
    ,   mPID(-1)
    ,   mStatus(STATUS_NOTLAUNCHED)
    ,   mExitCode(-1)
    ,   mProcessGroup(GROUP_INHERIT)
{
}

CProcess::CProcess(const std::string &pCmd,EOptions pOptions)
    :   mOptions(pOptions)
    ,   mPID(-1)
    ,   mStatus(STATUS_NOTLAUNCHED)
    ,   mExitCode(-1)
    ,   mProcessGroup(GROUP_INHERIT)
{
    std::vector<std::string> lCmd;
    split(pCmd,lCmd,fn::is_ascii_space(),nDeleteEmpty);
    SetCmd(lCmd);
}

CProcess::CProcess(const std::string &pCmd,const std::vector<std::string> &pArgs,EOptions pOptions)
    :   mOptions(pOptions)
    ,   mPID(-1)
    ,   mStatus(STATUS_NOTLAUNCHED)
    ,   mExitCode(-1)
    ,   mProcessGroup(GROUP_INHERIT)
{
    SetCmd(pCmd,pArgs);
}

CProcess::CProcess(const io::CURI &pCmd,const std::vector<std::string> &pArgs,EOptions pOptions)
    :   mOptions(pOptions)
    ,   mPID(-1)
    ,   mStatus(STATUS_NOTLAUNCHED)
    ,   mExitCode(-1)
    ,   mProcessGroup(GROUP_INHERIT)
{
    SetCmd(pCmd,pArgs);
}

CProcess::CProcess(const std::vector<std::string> &pCmd,EOptions pOptions)
    :   mOptions(pOptions)
    ,   mPID(-1)
    ,   mStatus(STATUS_NOTLAUNCHED)
    ,   mExitCode(-1)
    ,   mProcessGroup(GROUP_INHERIT)
{
    SetCmd(pCmd);
}

CProcess::~CProcess()
{
    if(mPID!=-1)
        mAllProcesses.erase(mPID);
}

/////////////////////////////////////////////////
//////////////////// interface //////////////////
/////////////////////////////////////////////////

void CProcess::SetCmd(const std::string &pCmd)
{
    mCmd=pCmd;
}

void CProcess::SetCmd(const std::string &pCmd,const std::vector<std::string> &pArgs)
{
    mCmd=pCmd;
    mArgs=pArgs;
}

void CProcess::SetCmd(const io::CURI &pCmd,const std::vector<std::string> &pArgs)
{
    mCmd=pCmd.Path();
    mArgs=pArgs;
}

void CProcess::SetCmd(const std::vector<std::string> &pArgs)
{
    mArgs.clear();
    if(!pArgs.size())
    {
        mCmd.clear();
        return;
    }
    
    mCmd=pArgs[0];
    mArgs.assign(pArgs.begin()+1,pArgs.end());
}

void CProcess::SetCmdArg(const std::string &pCmdArg)
{
    mCmdArg=pCmdArg;
}

void CProcess::SetOptions(EOptions pOptions)
{
    mOptions=pOptions;
}

void CProcess::SetWorkingDir(const io::CURI &pURI)
{
    mWD=pURI;
}

void CProcess::SetProcessGroup(pid_t pProcessGroup)
{
    mProcessGroup=pProcessGroup;
}

void CProcess::ClearArgs()
{
    mArgs.clear();
}

void CProcess::AddArg(const std::string &pArg)
{
    mArgs.push_back(pArg);
}

void CProcess::AddArgs(const std::vector<std::string> &pArgs)
{
    mArgs.insert(mArgs.end(),pArgs.begin(),pArgs.end());
}

void CProcess::AddRedirection(int pFrom,int pTo)
{
    SRedirection lR={pFrom,pTo};
    mRedirections.push_back(lR);
}

void CProcess::AddParentClose(int pFD)
{
    mParentClose.push_back(pFD);
}

io::PFDStream CProcess::GetErr()
{
    io::CPipe lPipe;
    lPipe.Create();
    AddRedirection(STDERR_FILENO,lPipe.GetOutputFD());
    AddParentClose(lPipe.GetOutputFD());
    return lPipe.GetInput();
}

io::PFDStream CProcess::GetOut()
{
    io::CPipe lPipe;
    lPipe.Create();
    AddRedirection(STDOUT_FILENO,lPipe.GetOutputFD());
    AddParentClose(lPipe.GetOutputFD());
    return lPipe.GetInput();
}

io::PFDStream CProcess::GetIn()
{
    io::CPipe lPipe;
    lPipe.Create();
    AddRedirection(STDIN_FILENO,lPipe.GetInputFD());
    AddParentClose(lPipe.GetInputFD());
    return lPipe.GetOutput();
}

io::PFDStream CProcess::GetOutErr()
{
    io::CPipe lPipe;
    lPipe.Create();
    AddRedirection(STDOUT_FILENO,lPipe.GetOutputFD());
    AddRedirection(STDERR_FILENO,lPipe.GetOutputFD());
    AddParentClose(lPipe.GetOutputFD());
    return lPipe.GetInput();
}

void CProcess::Launch()
{
    mPID=fork();
    mStatus=STATUS_RUNNING;

    if(mPID==0)                 //Child thread
        DoChild();
    else
        DoParent();
}

void CProcess::Wait(bool pCont)
{
    if(mStatus==STATUS_TERMINATED) return;
    
    int lOptions=WUNTRACED;
    if(pCont)
        lOptions|=WCONTINUED;
    
    int lStatus;
    if(waitpid(mPID,&lStatus,lOptions)<0)
    {
        throw XNotFound("error in CProcess::Wait()");
    }

    UpdateStatus(lStatus);
}

bool CProcess::Check()
{
    if(mStatus==STATUS_TERMINATED) return false;
    
    int lStatus;
    if(waitpid(mPID,&lStatus,WNOHANG|WUNTRACED|WCONTINUED)<0)
    {
        return false;
    }

    UpdateStatus(lStatus);
    return true;
}

int CProcess::Run(const std::string &pCommand)
{
    return system(pCommand.c_str());
}

void CProcess::DoChild()
{
    //restore the umask we had when the process was launched
    umask((mode_t)io::get_umask());
    
    if(mProcessGroup==GROUP_OWN)
        setpgid(0,0);
    else if(mProcessGroup!=GROUP_INHERIT)
        setpgid(0,mProcessGroup);

    if(mOptions&SET_TTY_PGRP)
    {
        os::CSignalBlocker lBlocker(SIGTTOU,SIGTTIN,SIGTSTP,SIGCHLD);
        
        tcsetpgrp(0,getpgrp());
    }

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant"
    std::signal(SIGINT,SIG_DFL);
    std::signal(SIGTSTP,SIG_DFL);
#pragma GCC diagnostic pop

    if(!mWD.empty())
    {
        if(chdir(mWD.Path().c_str())!=0)
            throw XPermission("can't chdir");
    }

    if(mOptions&DETACH)       //We must detach the thread... give it a new session
        setsid();

    for(int i=0;i<mRedirections.size();i++)
    {
        if(mRedirections[i].mTo==-1)
            close(mRedirections[i].mFrom);
        else if(dup2(mRedirections[i].mTo,mRedirections[i].mFrom)<0)
        {
            msg_error() << "can't duplicate file descriptor " << "\n";
            _exit(0);
        }
    }

    //TODO:close only the ones which aren't used
    for(int j=3;j<100;j++)
        close(j);

    std::string lShell;
    char *lArgs[mArgs.size()+10];

    int i=0;

    if(mOptions&TERMINAL)
    {
        lArgs[i++]=strdup("bterm");
    }
    if(mOptions&SHELL)
    {
        lShell=CEnv::Get("SHELL","/bin/sh");
        lArgs[i++]=strdup(lShell.c_str());
        lArgs[i++]=strdup("-c");
    }

    if(mCmdArg.empty()||!lShell.empty())
        lArgs[i++]=strdup(mCmd.c_str());
    else
        lArgs[i++]=strdup(mCmdArg.c_str());

    for(int j=0;j<mArgs.size();j++)
    {
        lArgs[i++]=strdup(mArgs[j].c_str());
    }

    lArgs[i++]=nullptr;

    if(!lShell.empty())
        execv(lShell.c_str(),lArgs);
    else if(mOptions&SEARCH_PATH)
        execvp(mCmd.c_str(),lArgs);
    else
        execv(mCmd.c_str(),lArgs);

    msg_error() << "can't execute " << mCmd << "\n";
    _exit(0);
}

void CProcess::DoParent()
{
    mAllProcesses[mPID]=this;
    //there is a recommendation in the rationale part of the setpgrp page of the POSIX:2008 specification, saying
    //that setting the process group should be done both in the child and the parent, so we do it here too!
    if(mProcessGroup==GROUP_OWN)
        setpgid(mPID,mPID);
    else if(mProcessGroup!=GROUP_INHERIT)
        setpgid(mPID,mProcessGroup);
    
    for(int i=0;i<mParentClose.size();i++)
    {
        close(mParentClose[i]);
    }
}

void CProcess::UpdateStatus(int pStatus)
{
    if(WIFEXITED(pStatus))
    {
        mStatus=STATUS_TERMINATED;
        mExitCode=WEXITSTATUS(pStatus);

        sExited(mExitCode);
    }
    else if(WIFSIGNALED(pStatus))
    {
        mStatus=STATUS_TERMINATED;
        mExitCode=-WTERMSIG(pStatus);

        sExited(mExitCode);
    }
    else if(WIFSTOPPED(pStatus))
    {
        mStatus=STATUS_STOPPED;
        sStopped();
    }
    else if(WIFCONTINUED(pStatus))
    {
        mStatus=STATUS_RUNNING;
        sContinued();
    }
}

CProcess *CProcess::WaitOne(bool pCont)
{
    int lOptions=WUNTRACED;
    if(pCont)
        lOptions|=WCONTINUED;
    
    while(true)
    {
        int lStatus;
        int lPID=waitpid(-1,&lStatus,lOptions);

        if(lPID<=0) continue;
        
        auto p=mAllProcesses.find(lPID);
        if(p==mAllProcesses.end()) continue;
        
        p->second->UpdateStatus(lStatus);
        return p->second;
    }
}

CProcess *CProcess::CheckOne()
{
    while(true)
    {
        int lStatus;
        int lPID=waitpid(-1,&lStatus,WNOHANG|WUNTRACED|WCONTINUED);

        if(lPID<=0) return nullptr;

        auto p=mAllProcesses.find(lPID);
        if(p==mAllProcesses.end()) continue;
        
        p->second->UpdateStatus(lStatus);
        return p->second;
    }
}

void CProcess::CheckAll()
{
    while(true)
    {
        int lStatus;
        int lPID=waitpid(-1,&lStatus,WNOHANG|WUNTRACED|WCONTINUED);

        if(lPID<=0) break;

        auto p=mAllProcesses.find(lPID);
        if(p==mAllProcesses.end()) continue;
        
        p->second->UpdateStatus(lStatus);
    }
}

/*namespace buola*/ }
